using System;

namespace Tools
{
    public class PerlinNoise3D
    {
        #region Private Fields

        private Single[, ,] noise;
        private Int32 noiseDimension;
        private Int32 noiseHeight;
        private Int32 seed;

        #endregion Private Fields

        #region Public Constructors

        public PerlinNoise3D(Int32 Seed, Int32 NoiseDimension, Int32 NoiseHeight)
        {
            this.seed = Seed;
            this.noiseDimension = NoiseDimension;
            this.noiseHeight = NoiseHeight;

            this.generateNoise();
        }

        public PerlinNoise3D(Int32 Seed)
            : this(Seed, 1024, 128)
        {
        }

        #endregion Public Constructors

        #region Public Methods

        public Single GetValue(Single x, Single y, Single z, Single Frequency, Single Amplitude)
        {
            Single value = this.getSmoothNoise(x * Frequency, y * Frequency, z * Frequency) * Amplitude;

            if (value < -1.0f) value = -1.0f;
            else if (value > 1.0f) value = 1.0f;

            return value;
        }

        public Single GetValue(Single x, Single y, Single z, Single Frequency, Single Amplitude, Single Persistance, Int32 Octaves)
        {
            Single value = 0;

            for (Int32 i = 0; i < Octaves; ++i)
            {
                value += this.getSmoothNoise(x * Frequency, y * Frequency, z * Frequency) * Amplitude;
                Frequency *= 2.0f;
                Amplitude *= Persistance;
            }

            if (value < -1.0f) value = -1.0f;
            else if (value > 1.0f) value = 1.0f;

            return value;
        }

        #endregion Public Methods

        #region Private Methods

        private void generateNoise()
        {
            this.noise = new Single[this.noiseDimension, this.noiseHeight, this.noiseDimension];
            Random noiseRandom = new Random(this.seed);

            for (Int32 x = 0; x < this.noiseDimension; ++x)
            {
                for (Int32 y = 0; y < this.noiseHeight; ++y)
                {
                    for (Int32 z = 0; z < this.noiseDimension; ++z)
                    {
                        this.noise[x, y, z] = ((Single)(noiseRandom.NextDouble()) - 0.5f) * 2.0f;
                    }
                }
            }
        }

        private Single getSmoothNoise(Single x, Single y, Single z)
        {
            Single FractionX = x - (Int32)x;
            Single FractionY = y - (Int32)y;
            Single FractionZ = z - (Int32)z;

            Int32 X1 = ((Int32)x + this.noiseDimension) % this.noiseDimension;
            Int32 Z1 = ((Int32)z + this.noiseDimension) % this.noiseDimension;
            Int32 Y1 = ((Int32)y + this.noiseHeight) % this.noiseHeight;

            Int32 X2 = ((Int32)x + this.noiseDimension - 1) % this.noiseDimension;
            Int32 Z2 = ((Int32)z + this.noiseDimension - 1) % this.noiseDimension;
            Int32 Y2 = ((Int32)y + this.noiseHeight - 1) % this.noiseHeight;

            Single value = 0;

            value += FractionX * FractionY * FractionZ * this.noise[X1, Y1, Z1];
            value += FractionX * FractionY * (1 - FractionZ) * this.noise[X1, Y1, Z2];
            value += FractionX * (1 - FractionY) * FractionZ * this.noise[X1, Y2, Z1];
            value += FractionX * (1 - FractionY) * (1 - FractionZ) * this.noise[X1, Y2, Z2];
            value += (1 - FractionX) * FractionY * FractionZ * this.noise[X2, Y1, Z1];
            value += (1 - FractionX) * FractionY * (1 - FractionZ) * this.noise[X2, Y1, Z2];
            value += (1 - FractionX) * (1 - FractionY) * FractionZ * this.noise[X2, Y2, Z1];
            value += (1 - FractionX) * (1 - FractionY) * (1 - FractionZ) * this.noise[X2, Y2, Z2];

            return value;
        }

        #endregion Private Methods
    }
}